package org.ihelpuoo.db;

import java.io.PrintWriter;
import java.lang.reflect.Constructor;
import java.sql.Connection;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;

import javax.sql.DataSource;

import org.apache.log4j.Logger;
import org.ihelpuoo.core.Factory;
import org.ihelpuoo.db.sql.SQLSet;
import org.ihelpuoo.lang.ConfigException;
import org.ihelpuoo.lang.ExceptionUtils;
import org.ihelpuoo.lang.InitializeException;
import org.ihelpuoo.lang.NutException;
import org.ihelpuoo.lang.StringUtils;
import org.ihelpuoo.util.Properties;

/**
 * <b>PoolFactory class:</b>
 * <hr color=grey size=1>
 * 
 * 
 * 
 * @author zozoh created @ 2006
 * 
 */
public final class ConnectionPool extends Factory implements DataSource
{

	private static Logger	log	= Logger.getLogger(ConnectionPool.class);

	public ConnectionPool()
	{
	}

	private List<PoolConnectionProxy>	useds;
	private List<PoolConnectionProxy>	idles;
	private SQLSet						sqls;
	protected String					url;
	protected String					userName;
	protected String					password;
	private int							size;
	private int							maxSize;
	private int							idleCount	= 0;
	private Class						proxyClass;
	private Constructor					proxyConstructor;
	private int							poolId;

	@Override
	public synchronized void init(Properties config) throws NutException
	{
		super.init(config);
		useds = new ArrayList<PoolConnectionProxy>();
		idles = new ArrayList<PoolConnectionProxy>();
		sqls = new SQLSet();

		String driver = StringUtils.trim(config.getString("driver"));
		String proxyClassName = config.getString("connection_proxy_class");
		try
		{
			if (null == proxyClassName)
			{
				proxyClass = PoolConnectionProxy.class;
			}
			else
			{
				proxyClass = Class.forName(StringUtils.trim(proxyClassName));
			}
			try
			{
				proxyConstructor = proxyClass.getDeclaredConstructor(new Class[]{this.getClass()});
			}
			catch (NoSuchMethodException e)
			{
				ConfigException te = new ConfigException(e);
				log.error(ExceptionUtils.dump(te));
				throw te;
			}
			Class.forName(driver);
		}
		catch (java.lang.ClassNotFoundException e)
		{
			ConfigException te = new ConfigException("Fail to init Driver: \"" + driver + "\"",e);
			log.error(ExceptionUtils.dump(te));
			throw te;
		}
		catch (SecurityException e)
		{
			ConfigException te = new ConfigException("Fail to init Proxy: \""
					+ proxyClass.getName() + "\"",e);
			log.error(ExceptionUtils.dump(te));
			throw te;
		}

		try
		{
			maxSize = Integer.parseInt(config.getString("max_size"));
		}
		catch (NumberFormatException e1)
		{
			maxSize = -1;
		}
		try
		{
			url = config.getString("url").trim();
			userName = config.getString("username").trim();
			password = config.getString("password").trim();
			size = Integer.parseInt(config.getString("size"));
			if (maxSize < size)
				maxSize = -1;
		}
		catch (Exception e)
		{
			ConfigException te = new ConfigException(e);
			log.error(ExceptionUtils.dump(te));
			throw te;
		}

		this.poolId = url.hashCode();
		if (log.isInfoEnabled())
			log.info("Pool[" + url + "](" + poolId + ") up! => size:" + size + " | max-size:"
					+ maxSize);

		String sfnsStr = config.getString("sqls");
		if (null != sfnsStr)
		{
			String[] sfns = sfnsStr.split("[;]");
			for (int i = 0; i < sfns.length; i++)
			{
				String name = StringUtils.trim(sfns[i]);
				if (null != name && name.length() > 0)
				{
					sqls.load(name);
				}
			}
		}
		try
		{
			buildConnctions();
		}
		catch (Exception e)
		{
			InitializeException te = new InitializeException(e);
			log.error(ExceptionUtils.dump(te));
			throw te;
		}
	}

	private void buildConnctions() throws SQLException
	{
		if (log.isInfoEnabled())
			log.info("Pool [" + url + "] build connection => " + size);
		for (int i = 0; i < size; i++)
		{
			idles.add(createConn());
		}
	}

	private PoolConnectionProxy createConn() throws SQLException
	{
		try
		{
			return (PoolConnectionProxy)this.proxyConstructor.newInstance(new Object[]{this});
		}
		catch (Exception e)
		{
			SQLException te = new SQLException(e.getMessage());
			log.error(ExceptionUtils.dump(te));
			throw te;
		}
	}

	public synchronized void reset() throws SQLException
	{
		depose();
		buildConnctions();
	}

	@Override
	public synchronized void depose()
	{
		if (log.isInfoEnabled())
		{
			log.info("Pool [" + url + "] deposed >> ");
			dumpConns("!depose!");
		}
		Iterator it;
		it = useds.iterator();
		while (it.hasNext())
		{
			((PoolConnectionProxy)it.next()).depose();
		}
		it = idles.iterator();
		while (it.hasNext())
		{
			((PoolConnectionProxy)it.next()).depose();
		}

		useds = new LinkedList<PoolConnectionProxy>();
		idles = new LinkedList<PoolConnectionProxy>();
		if (log.isInfoEnabled())
			log.info(" << DONE for Pool [" + url + "] deposed");
	}

	/**
	 * When close a connection, thats mean, it will return back a connection to
	 * pool. pool will check the used connection, if it less than the size pool
	 * will close parts of the idle connections. It is so called "malfluso"
	 * 
	 * @param conn:
	 *            ConnectionProxy object.
	 */
	public synchronized void close(PoolConnectionProxy conn)
	{
		if (log.isTraceEnabled())
			dumpConns("before close");
		if (useds.remove(conn))
			idles.add(conn);
		conn.setClosed(true);
		if (log.isDebugEnabled())
			dumpConns("close");
		this.notify();
	}

	/**
	 * When ebbtide, some connections in "idles" must will be used furture. So
	 * we just close parts of the connection in "idles". <br>
	 * <br>
	 * This method will be called in close() method. When close, it will call
	 * idles.notify() at first, so it will cause another Thread
	 * pickOneConnectionSafely() again, and pickOneConnectionSafely() will cause
	 * the used/idles changed, so ebbtid() should be synchronized.
	 */
	private void ebbtide()
	{
		int num = idles.size() - size > 0 ? size : 0;
		if (log.isDebugEnabled())
			log.debug("* ebbtide:" + num);
		for (int i = 0; i < num; i++)
		{
			PoolConnectionProxy c = idles.remove(0);
			c.depose();
		}
		if (log.isDebugEnabled())
			dumpConns("ebbtide");
	}

	private void dumpConns(String prefix)
	{
		log.info(String.format("%d: %15s => :%d\tI:%d | U:%d | &%d",new Object[]{poolId,prefix,
				(idles.size() + useds.size()),idles.size(),useds.size(),idleCount}));
	}

	/**
	 * Pick a ConnectionProxy object from the pool. <br>
	 * If can not got the connection, that's mean the pool had reached the max
	 * size. it should wait another Thread return back a connection object.
	 * 
	 * @return ConnectionProxy object
	 * @throws NoMoreConnectionException:
	 *             When allot new Connection SQLException rise or
	 *             InterruptedExcetpion rised when wait.
	 */
	public synchronized PoolConnectionProxy pick() throws NoMoreConnectionException
	{
		PoolConnectionProxy conn = null;
		if (log.isTraceEnabled())
			dumpConns("before pick");
		while (conn == null)
		{
			if (idles.size() == 0)
			{
				if (log.isDebugEnabled())
					log.debug("No idle...");
				if (maxSize < 0 || useds.size() < maxSize)
				{
					if (log.isDebugEnabled())
						log.debug("increase one connection");
					try
					{
						conn = createConn();
					}
					catch (SQLException e)
					{
						throw new NoMoreConnectionException(e);
					}
					useds.add(conn);
					conn.setClosed(false);
				}
			}
			else
			{
				conn = idles.remove(0);
				useds.add(conn);
				conn.setClosed(false);
			}
			if (conn == null)
			{
				if (log.isDebugEnabled())
					log.debug("waiting...");
				try
				{
					this.wait();
				}
				catch (InterruptedException e)
				{
					throw new NoMoreConnectionException(e);
				}
			}
			else
			{
				if (idles.size() > size)
				{
					if (idleCount > size)
					{
						ebbtide();
						idleCount = 0;
					}
					else
					{
						idleCount++;
					}
				}
				else
				{
					idleCount = 0;
				}
			}
		}
		if (log.isDebugEnabled())
			dumpConns("pick");
		try
		{
			conn.checkState();
		}
		catch (SQLException e)
		{
			conn.close();
			throw new NoMoreConnectionException(e);
		}
		return conn;
	}

	public SQLSet sqls()
	{
		return sqls;
	}

	public int getSize()
	{
		return size;
	}

	public int getMaxSize()
	{
		return maxSize;
	}

	public int getRealSize()
	{
		return idles.size() + useds.size();
	}

	public Connection getConnection()
	{
		return this.pick();
	}

	@Deprecated
	public Connection getConnection(String username, String password) throws SQLException
	{
		throw new SQLException(
				"Don't support this method,config the username and password by config file please!");
	}

	@Deprecated
	public PrintWriter getLogWriter() throws SQLException
	{
		throw new SQLException("Don't support this method!");
	}

	@Deprecated
	public int getLoginTimeout() throws SQLException
	{
		throw new SQLException("Don't support this method!");
	}

	@Deprecated
	public void setLogWriter(PrintWriter out) throws SQLException
	{
		throw new SQLException("Don't support this method!");
	}

	@Deprecated
	public void setLoginTimeout(int seconds) throws SQLException
	{
		throw new SQLException("Don't support this method!");
	}

	@Override
	public boolean isWrapperFor(Class<?> arg0) throws SQLException
	{
		return false;
	}

	@Override
	public <T> T unwrap(Class<T> arg0) throws SQLException
	{
		return null;
	}
}
